在看了 data 和 computed 的源码之后,如果仅仅只是单纯看源码,那么收获可能会比较小。对于 watch 的源码,换一种方式,结合之前的源码逻辑,先做出假设,再带着假设看源码。

如果是我来设计 watch 源码逻辑,我大致会这么做:

  • 首先,对 watch 对象进行遍历,对每个对象属性 watch-item 及值进行处理。
  • 然后,判断 watch-item 的属性值是对象还是函数,进而获取 watch-item 配置项 immediate、deep、handler。
  • 接着,对每个 watch-item 实例化 watcher 对象,然后将 handler 作为 watcher 实例的 getter 函数,一旦监听的数据变更,就通知该 watcher 进而触发该 getter 函数。
  • 最后,手动触发监听 data 数据的 getter 函数,这样 getter 就可以收集到当前的 watcher。

# initWatch 函数

function initState (vm) {
  vm._watchers = [];
  var opts = vm.$options;
  // ...
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}
function initWatch (vm, watch) {
  for (var key in watch) {
    var handler = watch[key];
    if (Array.isArray(handler)) {
      for (var i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i]);
      }
    } else {
      createWatcher(vm, key, handler);
    }
  }
}

initWatch 函数内部对 watch 进行遍历,获取 handler,然后就 handler 类型进行判断。如果 handler 是数组则在对 handler 进行遍历调用 createWatcher,否则直接调用 createWatcher。

handler 为数组类型没看源码之前没怎么留意这种写法,毕竟比较少用。官方文档传送门➡️ (opens new window)

// handler 为数组类型
export default {
  data() {
    return {
      num: 1
    }
  },
  watch: {
    num: {
      handler: [
        function(newVal) {
          // ...
        },
        function(newVal) {
          // ...
        }
      ]
    }
  }
}

# createWatcher 函数

function createWatcher (
  vm,
  expOrFn,
  handler,
  options
) {
  if (isPlainObject(handler)) { // 判断是否为普通对象
    options = handler;
    handler = handler.handler;
  }
  if (typeof handler === 'string') { // 如果 handler 为字符串,则将该字符串作为变量名获取 methods 中的函数
    handler = vm[handler];
  }
  return vm.$watch(expOrFn, handler, options) // 调用 $watch
}

上面函数跟最开始假设差不多。首先,判断 handler 是否为对象,获取 handler;如果 handler 为字符串,则将该字符串作为变量名获取 methods 中的函数;最后调用 vm.$watch 创建 watcher,该函数在最开始的 stateMixin 函数中已挂载到 Vue 原型上。

function stateMixin (Vue) {
  // ...

  Vue.prototype.$watch = function (
    expOrFn,
    cb,
    options
  ) {
    var vm = this;
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {};
    options.user = true;
    var watcher = new Watcher(vm, expOrFn, cb, options);
    if (options.immediate) {
      var info = "callback for immediate watcher \"" + (watcher.expression) + "\"";
      pushTarget(); // 当前 watcher 进栈
      invokeWithErrorHandling(cb, vm, [watcher.value], vm, info);
      popTarget(); // 当前 watcher 出栈
    }
    return function unwatchFn () {
      watcher.teardown();
    }
  };
}

首先,该函数会通过new Watcher()创建 watcher 对象实例。watcher 会保存 deep 信息,然后对 expOrFn 进行类型判断,由于 watch 要么是用函数的 key 作为 expOrFn,要么是用对象 keyPath 作为 expOrFn,所以都是字符串类型。然后对字符串类型的 expOrFn 做处理。

在调用 parsePath 函数之后,this.getter 就是 parsePath 返回的函数。然后,调用 this.get 函数,this.get 函数内部会将当前 watcher 进栈,同时将 Dep.target 设置为当前 watcher,然后调用 this.getter(也就是上面 parsePath 返回的函数),将当前的 vm 作为函数参数,进而触发监听数据的 getter 函数,实现 data 数据 watcher 的收集。

var Watcher = function Watcher (
 vm,
 expOrFn,
 cb,
 options,
 isRenderWatcher
) {
  // ...
  // options 处理

  // 解析表达式作为 getter
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  } else {
    this.getter = parsePath(expOrFn);
    if (!this.getter) {
      this.getter = noop;
      warn(
        "Failed watching path: \"" + expOrFn + "\" " +
        'Watcher only accepts simple dot-delimited paths. ' +
        'For full control, use a function instead.',
        vm
      );
    }
  }
  // 调用 get 函数
  this.value = this.lazy
    ? undefined
    : this.get();
}

# parsePath 函数

var unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/;
var bailRE = new RegExp(("[^" + (unicodeRegExp.source) + ".$_\\d]"));

function parsePath (path) {
  if (bailRE.test(path)) {
    return
  }
  var segments = path.split('.');
  return function (obj) {
    for (var i = 0; i < segments.length; i++) {
      if (!obj) { return }
      obj = obj[segments[i]];
    }
    return obj
  }
}

parsePath 函数主要逻辑:判断是否包含 unicode 编码,是的话则不做处理直接返回。接着对 path 通过.进行拆解,最后返回一个函数。其实 parsePath 函数就是一个闭包,外部调用返回函数可以拿到拆解后的 path,然后对将 path-item 作为传入对象 obj 的 key 值获取对象的值。

# 收集 watcher

this.get 函数会将当前 watcher 进栈,同时将 Dep.target 设置为当前 watcher,然后调用 this.getter(也就是上面 parsePath 返回的函数),将当前的 vm 作为函数参数 obj,进而触发监听数据的 getter 函数,实现 data 数据 watcher 的收集。

Watcher.prototype.get = function get() {
  pushTarget(this);
  var value;
  var vm = this.vm;
  try {
    value = this.getter.call(vm, vm); // 触发 parsePath 函数返回的函数,函数参数为当前 vm
  } catch (e) {
    if (this.user) {
      handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      // deep 为 true 时,调用 traverse 对 value 进行处理 
      traverse(value);
    }
    popTarget();
    this.cleanupDeps();
  }
  return value
};

另外,如果 deep 为 true 的话,还会调用 traverse 函数对 value 值进行处理。

# deep

对于 deep 为 true 的情况,会调用 traverse 函数对生成的 value 进行处理。traverse 函数会递归遍历一个对象,触发所有对象属性的 getter 函数,以便对象内的每个嵌套属性都被收集为“深度”依赖项。

traverse 相关源码如下:

var seenObjects = new _Set();

function traverse (val) {
  _traverse(val, seenObjects);
  seenObjects.clear();
}

function _traverse (val, seen) {
  var i, keys;
  var isA = Array.isArray(val);
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  // 判断是否已经处理收集过了
  if (val.__ob__) {
    var depId = val.__ob__.dep.id;
    if (seen.has(depId)) {
      return
    }
    seen.add(depId);
  }
  // 获取 val.xxx 会触发对应的 getter 函数,从而将当前的 watch-item-watcher 添加到对应的订阅者队列中
  if (isA) {
    i = val.length;
    while (i--) { _traverse(val[i], seen); } // val[i] 触发对应的 getter,在其 subs 中添加当前 watch-item-watcher
  } else {
    keys = Object.keys(val);
    i = keys.length;
    while (i--) { _traverse(val[keys[i]], seen); } // val[keys[i]] 触发对应的 getter,在其 subs 中添加当前 watch-item-watcher
  }
}

举个例子🌰:

export default {
  data() {
    return {
      person: {
        name: '',
        age: 18
      }
    }
  },
  watch: {
    person: {
      deep: true,
      handler(newVal) {
        // ...
      }
    }
  }
}

会为 person 实例化一个 watcher 对象,并将 handler 函数作为 watcher 对象的 getter 函数,因为 deep 为 true,在通过 traverse 函数处理后,会依次在 persion、person.age、person.age 的订阅者队列中添加当前 watcher。这样,一旦 person 对象中的任何属性值更新都会触发 watcher 的 getter 即 handler 函数的执行。